// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/renderer_host/overscroll_controller.h"

#include "base/command_line.h"
#include "base/logging.h"
#include "content/browser/renderer_host/overscroll_controller_delegate.h"
#include "content/public/browser/overscroll_configuration.h"
#include "content/public/common/content_switches.h"
#include "third_party/WebKit/public/platform/WebMouseWheelEvent.h"

using blink::WebInputEvent;

namespace {

bool IsScrollEndEffectEnabled()
{
    return base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
               switches::kScrollEndEffect)
        == "1";
}

bool IsGestureEventFromTouchpad(const blink::WebInputEvent& event)
{
    DCHECK(blink::WebInputEvent::isGestureEventType(event.type()));
    const blink::WebGestureEvent& gesture = static_cast<const blink::WebGestureEvent&>(event);
    return gesture.sourceDevice == blink::WebGestureDeviceTouchpad;
}

} // namespace

namespace content {

OverscrollController::OverscrollController()
    : overscroll_mode_(OVERSCROLL_NONE)
    , scroll_state_(STATE_UNKNOWN)
    , overscroll_delta_x_(0.f)
    , overscroll_delta_y_(0.f)
    , delegate_(NULL)
{
}

OverscrollController::~OverscrollController()
{
}

bool OverscrollController::ShouldProcessEvent(
    const blink::WebInputEvent& event)
{
    switch (event.type()) {
    case blink::WebInputEvent::MouseWheel:
        return false;
    case blink::WebInputEvent::GestureScrollBegin:
    case blink::WebInputEvent::GestureScrollUpdate:
    case blink::WebInputEvent::GestureScrollEnd: {
        const blink::WebGestureEvent& gesture = static_cast<const blink::WebGestureEvent&>(event);
        blink::WebGestureEvent::ScrollUnits scrollUnits;
        switch (event.type()) {
        case blink::WebInputEvent::GestureScrollBegin:
            scrollUnits = gesture.data.scrollBegin.deltaHintUnits;
            break;
        case blink::WebInputEvent::GestureScrollUpdate:
            scrollUnits = gesture.data.scrollUpdate.deltaUnits;
            break;
        case blink::WebInputEvent::GestureScrollEnd:
            scrollUnits = gesture.data.scrollEnd.deltaUnits;
            break;
        default:
            scrollUnits = blink::WebGestureEvent::Pixels;
            break;
        }

        return scrollUnits == blink::WebGestureEvent::PrecisePixels;
    }
    default:
        break;
    }
    return true;
}

bool OverscrollController::WillHandleEvent(const blink::WebInputEvent& event)
{
    if (!ShouldProcessEvent(event))
        return false;

    bool reset_scroll_state = false;
    if (scroll_state_ != STATE_UNKNOWN || overscroll_delta_x_ || overscroll_delta_y_) {
        switch (event.type()) {
        case blink::WebInputEvent::GestureScrollEnd:
            // Avoid resetting the state on GestureScrollEnd generated
            // from the touchpad since it is sent based on a timeout.
            reset_scroll_state = !IsGestureEventFromTouchpad(event);
            break;

        case blink::WebInputEvent::GestureFlingStart:
            reset_scroll_state = true;
            break;

        case blink::WebInputEvent::MouseWheel: {
            const blink::WebMouseWheelEvent& wheel = static_cast<const blink::WebMouseWheelEvent&>(event);
            if (!wheel.hasPreciseScrollingDeltas || wheel.phase == blink::WebMouseWheelEvent::PhaseEnded || wheel.phase == blink::WebMouseWheelEvent::PhaseCancelled) {
                reset_scroll_state = true;
            }
            break;
        }

        default:
            if (blink::WebInputEvent::isMouseEventType(event.type()) || blink::WebInputEvent::isKeyboardEventType(event.type())) {
                reset_scroll_state = true;
            }
            break;
        }
    }

    if (reset_scroll_state)
        scroll_state_ = STATE_UNKNOWN;

    if (DispatchEventCompletesAction(event)) {
        CompleteAction();

        // Let the event be dispatched to the renderer.
        return false;
    }

    if (overscroll_mode_ != OVERSCROLL_NONE && DispatchEventResetsState(event)) {
        SetOverscrollMode(OVERSCROLL_NONE);

        // Let the event be dispatched to the renderer.
        return false;
    }

    if (overscroll_mode_ != OVERSCROLL_NONE) {
        // Consume the event only if it updates the overscroll state.
        if (ProcessEventForOverscroll(event))
            return true;
    } else if (reset_scroll_state) {
        overscroll_delta_x_ = overscroll_delta_y_ = 0.f;
    }

    return false;
}

void OverscrollController::ReceivedEventACK(const blink::WebInputEvent& event,
    bool processed)
{
    if (!ShouldProcessEvent(event))
        return;

    if (processed) {
        // If a scroll event is consumed by the page, i.e. some content on the page
        // has been scrolled, then there is not going to be an overscroll gesture,
        // until the current scroll ends, and a new scroll gesture starts.
        if (scroll_state_ == STATE_UNKNOWN && (event.type() == blink::WebInputEvent::MouseWheel || event.type() == blink::WebInputEvent::GestureScrollUpdate)) {
            scroll_state_ = STATE_CONTENT_SCROLLING;
        }
        return;
    }
    ProcessEventForOverscroll(event);
}

void OverscrollController::DiscardingGestureEvent(
    const blink::WebGestureEvent& gesture)
{
    if (scroll_state_ != STATE_UNKNOWN && (gesture.type() == blink::WebInputEvent::GestureScrollEnd || gesture.type() == blink::WebInputEvent::GestureFlingStart)) {
        scroll_state_ = STATE_UNKNOWN;
    }
}

void OverscrollController::Reset()
{
    overscroll_mode_ = OVERSCROLL_NONE;
    overscroll_delta_x_ = overscroll_delta_y_ = 0.f;
    scroll_state_ = STATE_UNKNOWN;
}

void OverscrollController::Cancel()
{
    SetOverscrollMode(OVERSCROLL_NONE);
    overscroll_delta_x_ = overscroll_delta_y_ = 0.f;
    scroll_state_ = STATE_UNKNOWN;
}

bool OverscrollController::DispatchEventCompletesAction(
    const blink::WebInputEvent& event) const
{
    if (overscroll_mode_ == OVERSCROLL_NONE)
        return false;

    // Complete the overscroll gesture if there was a mouse move or a scroll-end
    // after the threshold.
    if (event.type() != blink::WebInputEvent::MouseMove && event.type() != blink::WebInputEvent::GestureScrollEnd && event.type() != blink::WebInputEvent::GestureFlingStart)
        return false;

    // Avoid completing the action on GestureScrollEnd generated
    // from the touchpad since it is sent based on a timeout not
    // when the user has stopped interacting.
    if (event.type() == blink::WebInputEvent::GestureScrollEnd && IsGestureEventFromTouchpad(event))
        return false;

    if (!delegate_)
        return false;

    gfx::Rect bounds = delegate_->GetVisibleBounds();
    if (bounds.IsEmpty())
        return false;

    if (event.type() == blink::WebInputEvent::GestureFlingStart) {
        // Check to see if the fling is in the same direction of the overscroll.
        const blink::WebGestureEvent gesture = static_cast<const blink::WebGestureEvent&>(event);
        switch (overscroll_mode_) {
        case OVERSCROLL_EAST:
            if (gesture.data.flingStart.velocityX < 0)
                return false;
            break;
        case OVERSCROLL_WEST:
            if (gesture.data.flingStart.velocityX > 0)
                return false;
            break;
        case OVERSCROLL_NORTH:
            if (gesture.data.flingStart.velocityY > 0)
                return false;
            break;
        case OVERSCROLL_SOUTH:
            if (gesture.data.flingStart.velocityY < 0)
                return false;
            break;
        case OVERSCROLL_NONE:
            NOTREACHED();
        }
    }

    float ratio, threshold;
    if (overscroll_mode_ == OVERSCROLL_WEST || overscroll_mode_ == OVERSCROLL_EAST) {
        ratio = fabs(overscroll_delta_x_) / bounds.width();
        threshold = GetOverscrollConfig(OVERSCROLL_CONFIG_HORIZ_THRESHOLD_COMPLETE);
    } else {
        ratio = fabs(overscroll_delta_y_) / bounds.height();
        threshold = GetOverscrollConfig(OVERSCROLL_CONFIG_VERT_THRESHOLD_COMPLETE);
    }

    return ratio >= threshold;
}

bool OverscrollController::DispatchEventResetsState(
    const blink::WebInputEvent& event) const
{
    switch (event.type()) {
    case blink::WebInputEvent::MouseWheel: {
        // Only wheel events with precise deltas (i.e. from trackpad) contribute
        // to the overscroll gesture.
        const blink::WebMouseWheelEvent& wheel = static_cast<const blink::WebMouseWheelEvent&>(event);
        return !wheel.hasPreciseScrollingDeltas;
    }

    // Avoid resetting overscroll on GestureScrollBegin/End generated
    // from the touchpad since it is sent based on a timeout.
    case blink::WebInputEvent::GestureScrollBegin:
    case blink::WebInputEvent::GestureScrollEnd:
        return !IsGestureEventFromTouchpad(event);

    case blink::WebInputEvent::GestureScrollUpdate:
    case blink::WebInputEvent::GestureFlingCancel:
        return false;

    default:
        // Touch events can arrive during an overscroll gesture initiated by
        // touch-scrolling. These events should not reset the overscroll state.
        return !blink::WebInputEvent::isTouchEventType(event.type());
    }
}

bool OverscrollController::ProcessEventForOverscroll(
    const blink::WebInputEvent& event)
{
    bool event_processed = false;
    switch (event.type()) {
    case blink::WebInputEvent::MouseWheel: {
        const blink::WebMouseWheelEvent& wheel = static_cast<const blink::WebMouseWheelEvent&>(event);
        if (!wheel.hasPreciseScrollingDeltas)
            break;
        event_processed = ProcessOverscroll(wheel.deltaX * wheel.accelerationRatioX,
            wheel.deltaY * wheel.accelerationRatioY, true);
        break;
    }
    case blink::WebInputEvent::GestureScrollUpdate: {
        const blink::WebGestureEvent& gesture = static_cast<const blink::WebGestureEvent&>(event);
        event_processed = ProcessOverscroll(
            gesture.data.scrollUpdate.deltaX, gesture.data.scrollUpdate.deltaY,
            gesture.sourceDevice == blink::WebGestureDeviceTouchpad);
        break;
    }
    case blink::WebInputEvent::GestureFlingStart: {
        const float kFlingVelocityThreshold = 1100.f;
        const blink::WebGestureEvent& gesture = static_cast<const blink::WebGestureEvent&>(event);
        float velocity_x = gesture.data.flingStart.velocityX;
        float velocity_y = gesture.data.flingStart.velocityY;
        if (fabs(velocity_x) > kFlingVelocityThreshold) {
            if ((overscroll_mode_ == OVERSCROLL_WEST && velocity_x < 0) || (overscroll_mode_ == OVERSCROLL_EAST && velocity_x > 0)) {
                CompleteAction();
                event_processed = true;
                break;
            }
        } else if (fabs(velocity_y) > kFlingVelocityThreshold) {
            if ((overscroll_mode_ == OVERSCROLL_NORTH && velocity_y < 0) || (overscroll_mode_ == OVERSCROLL_SOUTH && velocity_y > 0)) {
                CompleteAction();
                event_processed = true;
                break;
            }
        }

        // Reset overscroll state if fling didn't complete the overscroll gesture.
        SetOverscrollMode(OVERSCROLL_NONE);
        break;
    }

    default:
        DCHECK(blink::WebInputEvent::isGestureEventType(event.type()) || blink::WebInputEvent::isTouchEventType(event.type()))
            << "Received unexpected event: " << event.type();
    }
    return event_processed;
}

bool OverscrollController::ProcessOverscroll(float delta_x,
    float delta_y,
    bool is_touchpad)
{
    if (scroll_state_ != STATE_CONTENT_SCROLLING)
        overscroll_delta_x_ += delta_x;
    overscroll_delta_y_ += delta_y;

    float horiz_threshold = GetOverscrollConfig(
        is_touchpad ? OVERSCROLL_CONFIG_HORIZ_THRESHOLD_START_TOUCHPAD
                    : OVERSCROLL_CONFIG_HORIZ_THRESHOLD_START_TOUCHSCREEN);
    float vert_threshold = GetOverscrollConfig(
        OVERSCROLL_CONFIG_VERT_THRESHOLD_START);
    if (fabs(overscroll_delta_x_) <= horiz_threshold && fabs(overscroll_delta_y_) <= vert_threshold) {
        SetOverscrollMode(OVERSCROLL_NONE);
        return true;
    }

    // Compute the current overscroll direction. If the direction is different
    // from the current direction, then always switch to no-overscroll mode first
    // to make sure that subsequent scroll events go through to the page first.
    OverscrollMode new_mode = OVERSCROLL_NONE;
    const float kMinRatio = 2.5;
    if (fabs(overscroll_delta_x_) > horiz_threshold && fabs(overscroll_delta_x_) > fabs(overscroll_delta_y_) * kMinRatio)
        new_mode = overscroll_delta_x_ > 0.f ? OVERSCROLL_EAST : OVERSCROLL_WEST;
    else if (fabs(overscroll_delta_y_) > vert_threshold && fabs(overscroll_delta_y_) > fabs(overscroll_delta_x_) * kMinRatio)
        new_mode = overscroll_delta_y_ > 0.f ? OVERSCROLL_SOUTH : OVERSCROLL_NORTH;

    // The vertical oversrcoll currently does not have any UX effects other then
    // for the scroll end effect, so testing if it is enabled.
    if ((new_mode == OVERSCROLL_SOUTH || new_mode == OVERSCROLL_NORTH) && !IsScrollEndEffectEnabled())
        new_mode = OVERSCROLL_NONE;

    if (overscroll_mode_ == OVERSCROLL_NONE)
        SetOverscrollMode(new_mode);
    else if (new_mode != overscroll_mode_)
        SetOverscrollMode(OVERSCROLL_NONE);

    if (overscroll_mode_ == OVERSCROLL_NONE)
        return false;

    // Tell the delegate about the overscroll update so that it can update
    // the display accordingly (e.g. show history preview etc.).
    if (delegate_) {
        // Do not include the threshold amount when sending the deltas to the
        // delegate.
        float delegate_delta_x = overscroll_delta_x_;
        if (fabs(delegate_delta_x) > horiz_threshold) {
            if (delegate_delta_x < 0)
                delegate_delta_x += horiz_threshold;
            else
                delegate_delta_x -= horiz_threshold;
        } else {
            delegate_delta_x = 0.f;
        }

        float delegate_delta_y = overscroll_delta_y_;
        if (fabs(delegate_delta_y) > vert_threshold) {
            if (delegate_delta_y < 0)
                delegate_delta_y += vert_threshold;
            else
                delegate_delta_y -= vert_threshold;
        } else {
            delegate_delta_y = 0.f;
        }
        return delegate_->OnOverscrollUpdate(delegate_delta_x, delegate_delta_y);
    }
    return false;
}

void OverscrollController::CompleteAction()
{
    if (delegate_)
        delegate_->OnOverscrollComplete(overscroll_mode_);
    overscroll_mode_ = OVERSCROLL_NONE;
    overscroll_delta_x_ = overscroll_delta_y_ = 0.f;
}

void OverscrollController::SetOverscrollMode(OverscrollMode mode)
{
    if (overscroll_mode_ == mode)
        return;
    OverscrollMode old_mode = overscroll_mode_;
    overscroll_mode_ = mode;
    if (overscroll_mode_ == OVERSCROLL_NONE)
        overscroll_delta_x_ = overscroll_delta_y_ = 0.f;
    else
        scroll_state_ = STATE_OVERSCROLLING;
    if (delegate_)
        delegate_->OnOverscrollModeChange(old_mode, overscroll_mode_);
}

} // namespace content
